/*
 * Copyright 2012 Adrian Sulumberchean

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
 */

package ro.sands.requests.android.server;

import java.util.HashMap;
import java.util.Iterator;

import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.HttpVersion;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.conn.params.ConnManagerParams;
import org.apache.http.conn.scheme.PlainSocketFactory;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.HttpConnectionParams;
import org.apache.http.params.HttpParams;
import org.apache.http.params.HttpProtocolParams;
import org.apache.http.protocol.BasicHttpContext;
import org.apache.http.protocol.HttpContext;
import org.apache.http.util.EntityUtils;

import ro.sands.requests.android.tools.Util;
import android.util.Log;

/**
 * 
 * @author Adrian Sulumberchean
 *
 */
public class HttpRequest {

	private boolean logRequests = true;
	private boolean blockRequests = false;
	private long receivedTraffic;
	private long sentTraffic;

	private class PostThread extends Thread {

		private final HttpClient httpClient;
		private final HttpContext context;
		private final HttpPost httpPost;
		private final String requestId;
		private final SupremeResponseListener listener;
		private int reloginCounter = 0;

		public PostThread(HttpClient httpClient, HttpPost httpPost, String requestId, IResponseListener[] listeners) {
			this.httpClient = httpClient;
			this.context = new BasicHttpContext();
			this.httpPost = httpPost;
			this.requestId = requestId;
			this.listener = new SupremeResponseListener(listeners);
		}

		@Override
		public void run() {
			HttpResponse response = null;

			boolean retryRequest = true;

			reloginCounter = 0;

			while(retryRequest && keepRunningRequests){
				try {

					boolean responseReceived = false;

					if(!blockRequests){
						response = httpClient.execute(httpPost, context);
						responseReceived = true;
					}

					if(!blockRequests || responseReceived){
						retryRequest = false;
						keepRunningRequests = true;
					}

				} catch (Exception e) {
					e.printStackTrace();
					if(!blockRequests){
						synchronized (lock) {
							blockRequests = true;
						}

						boolean isInternetOn = Util.isNetworkConnected();

						if(isInternetOn){
							Util.log("try to relogin because internet is on...");

							retryRequest = true;
							keepRunningRequests = true;
							continue;
						}
						else{
							Util.log("internet off... relogin counter: " + reloginCounter);

							if(reloginCounter == 0){
								Util.log("show dialog for no internet connection");
							}

							Util.log("after no internet connection dialog shown, start checking network connection for 3 mins");
							// show Checking internet connection in status bar
							boolean checkConnection = true;
							long startTime = System.currentTimeMillis();

							while(checkConnection){
								if(System.currentTimeMillis() - startTime > 180000 || Util.isNetworkConnected()){
									checkConnection = false;
								}
								else{
									synchronized (lock) {
										try {
											lock.wait(10000);
										} catch (InterruptedException e1) {
											Util.log("", e1);
										}
									}
								}
							}

							if(Util.isNetworkConnected()){
								Util.log("retry request after internet back on...");
								
								retryRequest = true;
								keepRunningRequests = true;
								continue;
							}
							else{
								Util.log("internet is still off after 3 min ");
								
								retryRequest = false;
								continue;
							}
						}
					}
				}
			}

			if(response != null){
				processResponse(requestId, response.getEntity(), listener);
			}
		}
	}

	private HttpParams params;
	private HashMap<String, HttpClient> httpClients;
	private Object lock;
	private boolean keepRunningRequests = true;		// flag used to dismiss pending requests which remain when the connection to server is lost

	private static final int ESTABLISH_CONNECTION_TIMEOUT = 30*1000;	// set to 30 seconds
	private static final int RECEIVE_RESPONSE_TIMEOUT =  2*60*1000; 	// set to 2 minutes
	private static final int MAX_CONNECTIONS = 100;

	private static final String GENERAL_HTTP_CLIENT_ID = "-1";

	private static HttpRequest instance = null;

	public static HttpRequest getInstance(){
		if(instance == null){
			instance = new HttpRequest();
		}
		return instance;
	}

	private HttpRequest(){

		initHttpParams();

		httpClients = new HashMap<String, HttpClient>();
		httpClients.put(GENERAL_HTTP_CLIENT_ID, getNewHttpClient());

		lock = new Object();
		receivedTraffic = 0;
		sentTraffic = 0;

	}

	private void initHttpParams(){
		// Create and initialize HTTP parameterss
		params = new BasicHttpParams();

		ConnManagerParams.setMaxTotalConnections(params, MAX_CONNECTIONS);

		HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1);

		//set connection timeout
		HttpConnectionParams.setConnectionTimeout(params, ESTABLISH_CONNECTION_TIMEOUT); 
		HttpConnectionParams.setSoTimeout(params, RECEIVE_RESPONSE_TIMEOUT); 
		//		HttpConnectionParams.setSocketBufferSize(params, 80000);
	}

	private HttpClient getNewHttpClient(){

		// Create and initialize scheme registry 
		SchemeRegistry schemeRegistry = new SchemeRegistry();
		schemeRegistry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));

		// Create an HttpClient with the ThreadSafeClientConnManager.
		// This connection manager must be used if more than one thread will
		// be using the HttpClient.
		ThreadSafeClientConnManager cm = new ThreadSafeClientConnManager(params, schemeRegistry);

		return new DefaultHttpClient(cm, params);
	}

	public void request(String server, Request requestData,
			IResponseListener... responseListener){
		request(server, requestData, false, responseListener);
	}

	/**
	 * If a request that is suspended for a period of time on the server, you have to call setPendingRequestId() first.
	 * 
	 * @param requestData
	 * @param responseListeners
	 * @param force
	 */
	public void request(String serverURL, Request requestData, boolean force, 
			IResponseListener... responseListeners){
		// check for internet connection
		try {

			String requestId = requestData.getRequestId();

			if(force || !blockRequests){ 

				// create a thread for each request
				HttpPost httpPost = new HttpPost(serverURL);
				httpPost.setHeader("Content-Type","text/x-gwt-rpc; charset=utf-8");

				// set request data
				//					requestData.setRequestId(requestId + "-" +System.currentTimeMillis());

				String requestDataStr = requestData.getRequestStr();
				StringEntity stringEntity = new StringEntity(requestDataStr);
				sentTraffic += requestDataStr.getBytes().length;

				httpPost.setEntity(stringEntity);
				if(logRequests)
					Util.log(requestData.toString());

				HttpClient httpClient;
				if(httpClients.containsKey(requestId)){
					httpClient = httpClients.get(requestId);
				}
				else{
					httpClient = httpClients.get(GENERAL_HTTP_CLIENT_ID);
				}

				PostThread thread = new PostThread(httpClient, httpPost, requestId, responseListeners);
				thread.start();
			}
		} catch (Exception e) {
			Util.log("", e);
		}
	}

	public void release(){
		// When HttpClient instances is no longer needed, 
		// shut down the connection manager to ensure
		// immediate deallocation of all system resources
		Iterator<HttpClient> httpClientsIterator = httpClients.values().iterator();
		while(httpClientsIterator.hasNext()){
			httpClientsIterator.next().getConnectionManager().shutdown();
		}

		httpClients.clear();

		keepRunningRequests = true;

		instance = null;
	}

	private synchronized void processResponse(String requestId, HttpEntity responseEntity, IResponseListener responseListener){

		String jsonResponse;
		try {
			jsonResponse = EntityUtils.toString(responseEntity);
			receivedTraffic += jsonResponse.getBytes().length;

		} catch (Exception e) {
			Log.d("AndroidHttpRequest", "Could not read response.");
			e.printStackTrace();
			return;
		}

		if(logRequests)
			Log.d("AndroidHttpRequest", "Response id "+ requestId +": " + jsonResponse);

		if(responseListener != null){
			responseListener.responseReceived(requestId, jsonResponse);
		}
	}

	/**
	 * This is used to create a new HttpClient for the requests that are held on server (up to 1,5 sec)
	 * @param id
	 */
	public void setPendingRequestId(String id){
		if(!httpClients.containsKey(id)){
			httpClients.put(id, getNewHttpClient());
		}
	}

	public synchronized void setBlockRequests(boolean blockRequests) {
		this.blockRequests = blockRequests;
	}

	public synchronized boolean getBlockRequests() {
		return blockRequests;
	}

	public long getSentNetworkTraffic(){
		return sentTraffic;
	}

	public long getReceivedNetworkTraffic(){
		return receivedTraffic;
	}
}
